Вторая и третья часть задания: извлечение feature vector и применение градиентного бустинга¶

Начнем с извлечения feature vector¶

Загрузка библиотек:

In [1]:
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
# import torch.optim as optim
#import os
# import torch.nn.functional as F
import matplotlib.pyplot as plt
# from collections import defaultdict # to check distribution by classes
# from sklearn.metrics import precision_recall_fscore_support # to calculate F1 score
# from sklearn.model_selection import StratifiedShuffleSplit # to split images to train, val, and test
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
C:\Users\elena\AppData\Local\Programs\Python\Python310\lib\site-packages\tqdm\auto.py:22: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Загружаем лучший стейт обученной модели

In [2]:
transform = transforms.Compose([
    transforms.Resize(64),
    transforms.CenterCrop(64),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
image_dataset = torchvision.datasets.ImageFolder('images', transform=transform)
image_dataloader = torch.utils.data.DataLoader(image_dataset, batch_size=32, shuffle=False)

model = torchvision.models.resnet18(weights='ResNet18_Weights.IMAGENET1K_V1')
model.fc = nn.Linear(512, len(image_dataset.classes))
model.load_state_dict(torch.load('best_model_42_epoch_01_last.pt'))
image_classes = image_dataset.classes

Заменяем последний полносвязный уровень на единицы, чтобы извлечь признаки предпоследнего уровня для каждого изображения

In [3]:
model.fc_backup = model.fc
# model.fc = nn.Sequential()
model.fc = nn.Identity()
In [4]:
model.eval()
all_features = []
with torch.no_grad():
    for inputs, labels in image_dataloader:
        outputs = model(inputs)
        features = outputs
        all_features.append(features)
all_features = torch.cat(all_features, dim=0)
In [5]:
print(all_features)
print(all_features.size())
labels = np.concatenate([batch[1].numpy() for batch in image_dataloader])
tensor([[1.3572, 1.6053, 0.1278,  ..., 2.3075, 0.6772, 3.0499],
        [6.0935, 2.7589, 0.0000,  ..., 3.3483, 4.4636, 0.0000],
        [0.7585, 0.0000, 0.5369,  ..., 0.4460, 0.0000, 0.0000],
        ...,
        [0.0000, 0.0000, 1.3359,  ..., 0.0670, 2.0176, 0.0000],
        [0.0000, 1.7225, 2.1514,  ..., 0.0000, 0.0000, 1.4539],
        [0.0422, 2.4869, 1.3453,  ..., 0.8349, 0.2712, 0.0000]])
torch.Size([1422, 512])

Полученная матрица (тензор) feature vector имеет размерность количество изображений (1422) * количество нейронов (512) на последнем уровне.

Попробуем сделать кластеризацию различными алгоритмами¶

Сначала попробуем понизить размерность: используем PCA.

In [6]:
# используем StandardScaler для шкалирования наших данных
sc = StandardScaler()
X_scaled = sc.fit_transform(all_features)

# Apply PCA
pca = PCA(n_components=None)
pca.fit(X_scaled)

# Get the eigenvalues
# print("Eigenvalues:")
# print(pca.explained_variance_)
# print()

# Get explained variances
# print("Variances (Percentage):")
# print(pca.explained_variance_ratio_ * 100)
# print()

# Make the scree plot
plt.plot(np.cumsum(pca.explained_variance_ratio_ * 100))
plt.xlabel("Number of components (Dimensions)")
plt.ylabel("Explained variance (%)")
plt.hlines(y = 60, xmin = 0, xmax = 512, colors = 'g', linestyles = '--')
plt.show()

Можно увидеть, что первые главные компоненты объясняют достаточно мало дисперсии. Это значит, что признаки получились мало коррелированы между собой. Попробуем сделать кластеризацию методом к-средних на исходной матрице, количество кластеров выбираем 8, как и число классов изображения:

In [7]:
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=8, random_state=42)
kmeans.fit(all_features)
labels_kmeans = kmeans.labels_
centers = kmeans.cluster_centers_

from sklearn.metrics import adjusted_rand_score

labels == labels_kmeans
ari = adjusted_rand_score(labels, labels_kmeans)
print("Adjusted Rand Index: {:.4f}".format(ari))
C:\Users\elena\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\cluster\_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  warnings.warn(
Adjusted Rand Index: 0.1536

Для оценки точности используем функцию adjusted_rand_score, которая сама определяет наиболее вероятный лейбл кластера. Точность кластеризации получилась не очень высокая. Для визуализации и отрисовки лейблов используем UMAP.

In [8]:
import umap.umap_ as umap
embedding = umap.UMAP(n_neighbors=15,
                      min_dist=0.3,
                      metric='correlation').fit_transform(all_features)
In [9]:
# Plot the UMAP visualization with true labels

fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 6))
axes[0].scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', s=6)
axes[0].set_title('UMAP visualization with true labels')
# axes[0].colorbar()
# Plot the UMAP visualization with predicted labels
axes[1].scatter(embedding[:, 0], embedding[:, 1], c=labels_kmeans, cmap='tab10', s=6)
axes[1].set_title('UMAP visualization with predicted by kmeans labels')
# axes[1].colorbar()
# plt.colorbar(ax=axes)
fig.suptitle('k-means based on the original matrix, ARI = {:.4f}'.format(ari))

plt.show()

# plt.scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', s = 6)
# plt.colorbar()
# plt.title('UMAP visualization with true labels')
# plt.show()

# # Plot the UMAP visualization with predicted labels
# plt.scatter(embedding[:, 0], embedding[:, 1], c=labels_kmeans, cmap='tab10', s = 6)
# plt.colorbar()
# plt.title('UMAP visualization with predicted labels')
# plt.show()

Попробуем использовать алгоритм к-средних на матрице из первых 100 главных компонент

In [10]:
# from sklearn.decomposition import PCA

pca = PCA(n_components=100)
pca_data = pca.fit_transform(all_features)

# Perform k-means clustering on the PCA components
kmeans_pca = KMeans(n_clusters=8, random_state=42)
kmeans_pca.fit(pca_data)

# Print the cluster labels assigned to each data point
print(kmeans_pca.labels_)

ari_pca = adjusted_rand_score(labels, kmeans_pca.labels_)
print("Adjusted Rand Index: {:.4f}".format(ari_pca))
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 6))
axes[0].scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', s=6)
axes[0].set_title('UMAP visualization with true labels')
# axes[0].colorbar()
# Plot the UMAP visualization with predicted labels
axes[1].scatter(embedding[:, 0], embedding[:, 1], c=kmeans_pca.labels_, cmap='tab10', s=6)
axes[1].set_title('UMAP visualization with predicted by kmeans labels')
fig.suptitle('k-means based on first 100 PC, ARI = {:.4f}'.format(ari_pca))

# axes[1].colorbar()
# plt.colorbar(ax=axes)
plt.show()
C:\Users\elena\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\cluster\_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  warnings.warn(
[5 3 5 ... 4 7 5]
Adjusted Rand Index: 0.1568

Точность кластеризации не улучшилась после использования главных компонент, возможно потому что исходные переменные не были сильно скоррелированы друг с другом. Попробуем использовать иерархическую кластеризацию

In [11]:
# import numpy as np
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster

# Perform hierarchical clustering
Z = linkage(all_features, method='ward')

# Cut the dendrogram to 8 groups
labels_hier = fcluster(Z, 8, criterion='maxclust')
color_threshold = Z[-7, 2]
# Plot the dendrogram to visualize the hierarchy

dendrogram(Z, color_threshold=color_threshold)
plt.title('Dendrogram')
plt.xlabel('Sample index')
plt.ylabel('Distance')
plt.show()


# Compare the resulting labels with the true labels
score = adjusted_rand_score(labels, labels_hier)
print('Adjusted Rand score:', score)
Adjusted Rand score: 0.1625178276870089

Теперь попробуем использовать метод k-ближайших соседей. Загрузим те же самые индексы тренировочного датасета и тестового + валидационного, чтобы избежать утечки данных.

In [12]:
train_index = np.load('train_index_42.npy')
train_index
test_index = np.load('test_index_42.npy')
print(test_index)
X_train = all_features[train_index]
X_test = all_features[test_index]
y_train = labels[train_index]
y_test = labels[test_index]
[ 640  295 1277  734  897 1304   79  955  908  116  252  660 1187  716
 1382 1017 1147 1121  663 1077 1181  592  822  990  353  927 1262  430
  530 1311  391  778  515 1172  396 1397 1162 1190 1079   35 1018 1049
 1010  967  291  618  503  826 1282  277 1416 1076 1324  722 1066 1111
 1313 1275  605  375  771  357  266  192  747  165  590  810  619  307
  670  934  553    1  558  675  936  788  981  784  280 1207  656  325
  567  916  344  180  451  198 1150  940  880   92  966  607   97 1280
  697  621  347 1153   42  458   10  615  893  931  498  947    7  193
 1045 1056  668 1084 1003  860  172  100 1052  878  657  235  145  704
  118  653 1320  600  951  129 1071  410 1240 1235 1288  112  885  316
 1323 1059 1182 1194 1388 1095  439  383 1296  673 1264  652  236 1136
 1087  650  243   52 1041 1270 1286  939  109  509  323  914  633 1417
  424 1168  701  958  389  278  841  126  328  699  576  324 1409   57
  305  106  200  814  876  494 1373 1242  993  195 1384   93 1126 1244
 1009  105  906   69  402  473  522  862   39  905 1127  163  639  614
  272  898  802  270  297  973  360 1048  350  433  262   51  282 1254
    2  635 1141  573  463  462  715  720   77  203  710  725  365  355
  185  523  779  903 1091   21  863 1239 1099 1291  201  127  787  513
 1368 1113  767  702  589  597  674  114  855  984   28 1369  130 1053
  768  417  170 1133  514  529 1319  334 1367  431 1258 1391 1051 1119
   18  696 1241 1272 1006  770  638 1261 1294 1047  824  583  255  764
  452 1166  332 1022  405 1396  611  774  818  279  234 1335  998 1040
  729  286  289  752  441  755 1130 1131 1176  569 1404 1158  557  254
  224 1177 1144   54 1380  427  429 1274 1410  445  672 1033  942  803
  782 1050  912 1152 1339 1214   22 1307  409  207  271  570   99  374
   14  624  301  368 1201  229  251 1352  815  728  680  398 1322  335
  160  381  620 1024  108 1309 1408 1300 1316  608  609  525  468 1101
  476 1257  679  692   38  321 1266  983  496  581  265  695  751  724
 1299 1353  649  472  227  732   20   73  453   23  861  319  505  226
  418  626  754   66 1357  798  985 1215 1179  281  938  150  117 1385
  847  221  830 1358  199 1008 1210]
In [13]:
from sklearn.neighbors import KNeighborsClassifier
# from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# X_train, X_test, y_train, y_test = train_test_split(all_features, labels, test_size=0.2, random_state=66)
# k = 16  # сработало хорошо
k = 15
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X_train, y_train)

y_pred = knn.predict(X_test)
report = classification_report(y_test, y_pred, zero_division=0)
print(report)

acc = np.mean(y_pred == y_test)
print("Classification accuracy: {:.2f}%".format(acc * 100))
              precision    recall  f1-score   support

           0       1.00      0.16      0.27        19
           1       0.55      0.92      0.69       110
           2       0.57      0.71      0.63        72
           3       0.76      0.21      0.33        61
           4       0.57      0.66      0.61        65
           5       0.69      0.69      0.69        35
           6       1.00      0.22      0.36        23
           7       0.72      0.31      0.43        42

    accuracy                           0.59       427
   macro avg       0.73      0.48      0.50       427
weighted avg       0.66      0.59      0.55       427

Classification accuracy: 59.25%

Метод k-ближайших соседей отработал лучше, чем k-means, поэтому попробуем визуализировать картинки с соответствующим лейблом. По очереди отрисуем картинки, соответствующие предсказанным лейблам.

In [14]:
def show_images(indices, nrow=4,ncol=4, title = ''):
    images = [image_dataset[idx][0] for idx in indices]
    curr_labels = labels[indices]
    curr_labels = (np.take(image_classes, curr_labels))
    fig, axs = plt.subplots(nrows=nrow, ncols=ncol, figsize=(21, 12))
#     plt.title('title')

# iterate over the subplots and plot the images and labels
    for i, ax in enumerate(axs.flat):
        # plot the image
        imgs = images[i].numpy().transpose((1, 2, 0))
        mean = np.array([0.485, 0.456, 0.406])
        std = np.array([0.229, 0.224, 0.225])
        imgs = std * imgs + mean
        imgs = np.clip(imgs, 0, 1)
        ax.imshow(imgs)
        ax.set_title(str(curr_labels[i]))
        ax.axis("off")
    fig.suptitle(title, fontsize=18)
    plt.show()
In [15]:
test_index[y_pred == 0]
Out[15]:
array([52, 28, 23], dtype=int64)
In [30]:
show_images(test_index[y_pred == 0], 1, len(test_index[y_pred == 0]), 'Для артдеко характерно наличие картинки по центру, по краям фон')

Первый класс изображений - предсказанный алгоритмом к-ближайших соседей АртДеко. Определилось три изображения, все относятся к нужному классу. Можно отметить наличие изображения по центру, в то время как по краям фон. Возможно, это являлось признаком, на которые обращает внимание модель. Отрисуем следующий класс изображений: кубизм

In [29]:
show_images(test_index[y_pred == 1], 6, 6, 'Для кубизма сложно найти ярко выраженные фичи')

Видно, что многие изображения определились правильно, однако очень много ложно-позитивных срабатываний. В отличие от предыдущего случая, здесь изображения заполнены содержанием по всей площади. Цвета яркие. Я смотрела сама изображения в папке кубизм, туда попал в том числе Сезанн, который скорее в стиле постимпрессионизма пишет. Например, здесь это изображение во второй строке четвертого столбца (paul-cezanne_chateau-noir-1). Подобные изображения могли затруднить идентификацию признаков изображений классического кубизма (Малевич). Перейдем к импрессионизму

In [28]:
print(len(test_index[y_pred == 2]))
show_images(test_index[y_pred == 2], 6, 6, 'Для импрессионизма характерны изображения природы')
90

Особенности картин, которые классифицированы как импрессионизм ярко выделяются: изображения природы, деревьев, озер. По этой причине натурализм и фотографии, где изображена природа ошибочно классифицированы как импрессионизм. Например 5 строка 1 столбец, фотография на неискушенный глаз похожа на картину импрессионистов, особенно при таком низком разрешении. Так что неудивительно, что нейросеть тоже ошиблась :)

In [25]:
print(len(test_index[y_pred == 3]))
show_images(test_index[y_pred == 3], 4, 4, 'Для японизма возможно ключевым признаком является бежевый фон')
17

Кажется что нейросеть определелиа для японизма характерным свойством бежевый светлый фон, возможно как в той истории про то как волков от собак отличали по снежному фону у волков. Это объясняет почему сюда попал кубизм (2 строка 3 столбец), птичка (3, 3) и розовая бутылка (3, 2)

In [27]:
show_images(test_index[y_pred == 4], 6, 6, 'В натурализме много цветов и растений')

Для натурализма характерны такие формы как цветы и листья. Есть несколько странных ложнопозитивных срабатываний, например машина, что сложно интерпретировать.

In [26]:
show_images(test_index[y_pred == 5], 6, 5, 'Большинство изображений стиля рококо содержат лица')

Одно из самых ярких признаков - Рококо. Почти на всех изображениях есть человек, в особенности лицо. Это объясняет почему фотографии с лицами людей определились в рококо. Кроме того, любопытно что нейросеть распознает и группы людей, где лица некрупным планом. Можно сказать, что для нейросети определящим признаком, что изображение относится к рококо, является наличие людей.

In [23]:
show_images(test_index[y_pred == 6], 1, len(test_index[y_pred == 6]), 'Для комиксов (cartoon) характерны яркие цвета')

Картинок с мультиками немного, но они корректно определились. Характерным свойством является яркие, даже слишком цвета. К сожалению, выборка в исходном датасете небольшая.

In [24]:
print(len(test_index[y_pred == 7],))
show_images(test_index[y_pred == 7], 3, 6, image_classes[7])
18

Похоже для нейросети важным признаком фотографии являются машинки, мотициклы, в целом транспорт и коты. Некоторые изображения определились достаточно неочевидно, например артдеко и японизм, которых ложно записали сюда. Но в целом логика прослеживается.

Можно заметить, что извлеченные изображения достаточно контрастные, хотя кубизм оказался достаточно шумным, без ярко выраженных фичей. Возможно, стоит увеличить выборку.

Третья часть задания: выполняем градиентный бустинг на извлеченные фичи¶

In [32]:
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score
from sklearn.metrics import confusion_matrix, f1_score

# Create a gradient boosting classifier
gb = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42)

# Train the classifier on the training set
gb.fit(X_train, y_train)

# Make predictions on the test set
y_pred = gb.predict(X_test)
confusion_matrix_gb = confusion_matrix(y_test, y_pred)
print('Confusion Matrix:')
print(confusion_matrix_gb)

# Calculate F1 score
f1 = f1_score(y_test, y_pred, average='weighted')
# Calculate the accuracy of the model
accuracy = accuracy_score(y_test, y_pred)
print('Accuracy: {:.4f}, F1-score wieghted: {:.4f}'.format(accuracy, f1))
Confusion Matrix:
[[ 3  8  0  6  2  0  0  0]
 [ 1 93  4  3  2  0  2  5]
 [ 0  8 50  2  9  3  0  0]
 [ 0 12  3 38  4  0  1  3]
 [ 2  6 11  4 35  2  1  4]
 [ 0  0  6  0  8 20  0  1]
 [ 0  4  7  4  1  1  5  1]
 [ 0  5  6  1  4  4  1 21]]
Accuracy: 0.6206, F1-score wieghted: 0.6051
In [33]:
dataframe = pd.DataFrame(confusion_matrix_gb, index=image_classes, columns=image_classes)
plt.figure(figsize=(8, 6))

# Create heatmap
sns.heatmap(dataframe, annot=True, cbar=None,cmap="YlGnBu",fmt="d")
plt.title("Confusion Matrix of gradient boosting"), plt.tight_layout()

plt.ylabel("True Class"), 
plt.xlabel("Predicted Class")
plt.show()

Точность и F1-score алгоритма градиентного бустинга чуть ниже чем у нейронной сети. Для нейросети в этом наборе параметров получилось: Точность: 0.6842, Weighted F1-Score: 0.6811 Попробую использовать алгоритм xgboost

In [34]:
import xgboost as xgb

xgb_model = xgb.XGBClassifier(n_estimators=100, learning_rate=0.3, max_depth=3, random_state=42)

# Train the model
xgb_model.fit(X_train, y_train)

# Make predictions on the test set
y_pred = xgb_model.predict(X_test)

# Calculate the confusion matrix
confusion_matrix_xgb = confusion_matrix(y_test, y_pred)
print('Confusion Matrix:')
print(confusion_matrix_xgb)

# Calculate F1 score
f1 = f1_score(y_test, y_pred, average='weighted')
# print(f'F1 score: {f1:.3f}')

# Calculate the accuracy of the model
accuracy = accuracy_score(y_test, y_pred)
# print(f'Accuracy: {accuracy:.3f}')
print('Accuracy: {:.4f}, F1-score wieghted: {:.4f}'.format(accuracy, f1))
Confusion Matrix:
[[ 5  9  0  1  2  0  2  0]
 [ 4 91  3  1  6  0  1  4]
 [ 0 13 43  2 10  3  1  0]
 [ 1 10  4 38  4  1  0  3]
 [ 1  5  9  5 39  2  0  4]
 [ 0  0  5  0  7 23  0  0]
 [ 0  4  4  5  0  0  8  2]
 [ 0  5  4  0  5  2  0 26]]
Accuracy: 0.6393, F1-score wieghted: 0.6324
In [35]:
dataframe = pd.DataFrame(confusion_matrix_xgb, index=image_classes, columns=image_classes)
plt.figure(figsize=(8, 6))

# Create heatmap
sns.heatmap(dataframe, annot=True, cbar=None,cmap="YlGnBu",fmt="d")
plt.title("Confusion Matrix of xgboost"), plt.tight_layout()

plt.ylabel("True Class"), 
plt.xlabel("Predicted Class")
plt.show()

xgboost отработал несколько лучше чем простой градиентный бустинг. Однако softmax в нейросети имеет не меньшую точность и F1 score, по крайней мере при этом наборе параметров.